package assg.funnycells.server;

import java.util.*;
import java.util.regex.*;

import assg.funnycells.util.*;
import assg.funnycells.common.*;

public class ServerState {
    ActivityDisplay display;
    Configuration config;
    Logger logger;
    int height, width;
    Hashtable<String, Cell> cells;
    ArrayList<Chemical>[][] chemicalMap;
    ArrayList<String>[][] cellMap;

    int molEnergy, movEnergy, senseEnergy, infectionEnergy, immunisationEnergy, senseRad, molTimeout, jiggle, quorumSize, stateDropTimeout,
	pheromoneLifetime, transferEnergy, updateWindow;
    String anyStart, infectedStart, antibodyStart;
    String energyAffector, energyReceptor;

    int numCells, numSusceptible, numInfected, numAntibodies, numInfections, numImmunisations;
    Hashtable<String, Double> infectedMoves, antibodyMoves;
    double lastInfectedEnergyUse, infectedEnergyUse, lastAntibodyEnergyUse, antibodyEnergyUse;
    double energyAlpha;

    long lastWindowUpdate = 0;
    long simulationStart = 0;
    long termination = 0;

    Pattern dnaPattern;

    public ServerState(ActivityDisplay display) {
	this.display = display;
	config = Coordinator.getConfig();
	height = config.getIntVal(Configuration.MAP, Configuration.HEIGHT);
	width = config.getIntVal(Configuration.MAP, Configuration.WIDTH);
	cells = new Hashtable<String, Cell>();
	chemicalMap = (ArrayList[][])(new ArrayList[height][width]);
	cellMap = (ArrayList[][])(new ArrayList[height][width]);
	infectedMoves = new Hashtable<String, Double>();
	antibodyMoves = new Hashtable<String, Double>();
	logger = Logger.getDefaultLogger();

	numCells = 0;
	numSusceptible = 0;
	numInfected = 0;
	numAntibodies = 0;
	numInfections = 0;
	numImmunisations = 0;

	molEnergy = config.getIntVal(Configuration.PARAMS, Configuration.MOLENERGY);
	movEnergy = config.getIntVal(Configuration.PARAMS, Configuration.MOVENERGY);
	senseEnergy = config.getIntVal(Configuration.PARAMS, Configuration.SENSEENERGY);
	infectionEnergy = config.getIntVal(Configuration.PARAMS, Configuration.INFECTIONENERGY);
	immunisationEnergy = config.getIntVal(Configuration.PARAMS, Configuration.IMMUNISATIONENERGY);
	transferEnergy = config.getIntVal(Configuration.PARAMS, Configuration.TRANSFERENERGY);
	senseRad = config.getIntVal(Configuration.PARAMS, Configuration.SENSERADIUS);
	molTimeout = config.getIntVal(Configuration.PARAMS, Configuration.MOLTIMEOUT);
	jiggle = config.getIntVal(Configuration.PARAMS, Configuration.JIGGLE);
	quorumSize = config.getIntVal(Configuration.PARAMS, Configuration.QUORUMSIZE);
	stateDropTimeout = config.getIntVal(Configuration.PARAMS, Configuration.MOLTIMEOUT) * 
	    config.getIntVal(Configuration.PARAMS, Configuration.DROPTIMEOUT);
	pheromoneLifetime = config.getIntVal(Configuration.PARAMS, Configuration.MOLTIMEOUT) *
	    config.getIntVal(Configuration.PARAMS, Configuration.PHEROMONETIMEOUT);
	updateWindow = config.getIntVal(Configuration.PARAMS, Configuration.UPDATEWINDOW);
	energyAlpha = config.getDoubleVal(Configuration.PARAMS, Configuration.ENERGYALPHA);
	
	// assuming some movement in the -1th window
	//	lastInfectedEnergyUse = (double)movEnergy * updateWindow / molTimeout / config.getIntVal(Configuration.PARAMS, Configuration.RATETIMEOUT);
	//	lastAntibodyEnergyUse = lastInfectedEnergyUse;
	lastInfectedEnergyUse = 0;
	lastAntibodyEnergyUse = 0;

	anyStart = config.getVal(Configuration.RECEPTORS, Configuration.START) + 
	    config.getVal(Configuration.RECEPTORS, Configuration.ANY);
	infectedStart = config.getVal(Configuration.RECEPTORS, Configuration.START) +
	    config.getVal(Configuration.RECEPTORS, Configuration.INFECTED);
	antibodyStart = config.getVal(Configuration.RECEPTORS, Configuration.START) +
	    config.getVal(Configuration.RECEPTORS, Configuration.ANTIBODY);

	energyAffector = config.getVal(Configuration.RECEPTORS, Configuration.START) +
	    config.getVal(Configuration.RECEPTORS, Configuration.ANY) +
	    config.getVal(Configuration.AFFECTORS, Configuration.ENERGY);
	energyReceptor = config.getVal(Configuration.RECEPTORS, Configuration.START) +
	    config.getVal(Configuration.RECEPTORS, Configuration.ANY) +
	    config.getVal(Configuration.AFFECTORS, Configuration.ENERGY);
	
	dnaPattern = Pattern.compile("[^atcg]");
    }
    
    public void cellCrash(String cellId) {
	if(!cellId.equals(Cell.UNKNOWN)) {
	    removeCell(cellId);
	}
    }

    public void newCell(String cellId, int type, int x, int y, int energy, Cell cell) throws Exception {
	if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN) && type != -1 && x != -1 && y != -1) {
	    if(cells.get(cellId) != null) {
		// XXX send cell exists message. For now, just let it run and re-register
		logger.info("NEWCELL: " + cellId + " exists");
		removeCell(cellId);
	    }
	    cell.setId(cellId);
	    CellState cellState = cell.getCellState();
	    cellState.setType(type);
	    cellState.setPos(x, y);
	    cellState.setCellId(cellId);
	    cellState.setEnergy(energy);
	    addCell(cellId, cell);

	    if(simulationStart == 0) {
		simulationStart = lastWindowUpdate = System.currentTimeMillis();
		if(termination > 1) {
		    new Thread(
			       new Runnable() {
				   public void run() {
				       display.startTimer((int)(termination / 1000));
				       try {
					   Thread.sleep(termination);
				       } catch(Exception e1) { }
				       try {
					   exitSimul();
					   System.exit(0);
				       } catch(Exception e2) {
					   System.exit(-1);
				       }
				   }
			       }
			       ).start();
		}
	    }

	    logger.infooverride("NEWCELL: " + cellId + " (" + x + ", " + y + ") + type = " + type + ": " + System.currentTimeMillis());
	    sendAck(Cell.NEWCELL, cellState, cell);
	} else {
	    logger.info("NEWCELL: " + cellId + " error bad arguments in cell initialization");
	    cell.sendMessage(new String[]{Cell.ERROR, "Bad arguments in cell initialization"});
	}
    }

    public void moveCell(String cellId, int incX, int incY) throws Exception {
	if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN) && incX >= -1 && incY >= -1 && 
	   incX <= 1 && incY <= 1 && cells.get(cellId) != null) {
	    Cell cell = cells.get(cellId);
	    CellState cellState = cell.getCellState();
	    if(cellState.energy() >= movEnergy) {
		logger.info("Got request MOVECELL: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") -> " + 
			    incX + ":" + incY);
		if(cellState.getX() + incX < 0 || cellState.getX() + incX >= width ||
		   cellState.getY() + incY < 0 || cellState.getY() + incY >= height) {
		    logger.info("MOVECELL: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") error hit wall");
		    cell.sendMessage(new String[]{Cell.WALL, "Illegal move"});
		} else {
		    cellMapRemove(cellId, cellState.getX(), cellState.getY());
		    cellState.setPos(cellState.getX() + incX, cellState.getY() + incY);
		    if(incX != 0 && incY != 0) {
			cellState.setEnergy(cellState.energy() - movEnergy);
			if(cellState.type() == CellState.INFECTED) {
			    infectedEnergyUse += ((double)movEnergy / numInfected);
			} else if(cellState.type() == CellState.ANTIBODY) {
			    antibodyEnergyUse += ((double)movEnergy / numAntibodies);
			}
			updateWindowedMetrics();
			updateCoordinationMoves(cellState);
		    }

		    cellMapAdd(cellId, cellState.getX(), cellState.getY());
		    
		    if(cellState.type() == CellState.SUSCEPTIBLE) {
			if(changeState(cellState, CellState.ANTIBODY))
			    return;
			if(changeState(cellState, CellState.INFECTED))
			    return;
		    }
		    else if(cellState.type() == CellState.ANTIBODY) {
			if(changeState(cellState, CellState.INFECTED))
			    return;
		    }
		    else if(cellState.type() == CellState.INFECTED) {
			if(changeState(cellState, CellState.ANTIBODY))
			    return;
		    }

		    //		    printCellMap();
		    
		    logger.info("MOVECELL: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") Ok");
		    sendAck(Cell.MOVECELL, cellState, cell);
		}
	    } else {
		logger.info("MOVECELL: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") error not enough energy to move");
		cell.sendMessage(new String[]{Cell.ERROR, "Not enough energy to move"});
		cell.sendMessage(new String[]{Cell.EXIT + "=" + cellId});
	    }
	} else {
	    if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN)) {
		Cell cell = cells.get(cellId);
		if(cell != null) {
		    logger.info("MOVECELL: " + cellId + "(" + cell.getCellState().getX() + ", " + 
				cell.getCellState().getY() + ") error bad arguments in cell move");
		    cell.sendMessage(new String[]{Cell.ERROR, "Bad arguments in cell move"});
		}
	    }
	}
    }
    
    public void dropChem(String cellId, String chemStr, int offX, int offY, String color) throws Exception {
	if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN) && !chemStr.equals("") && cells.get(cellId) != null) {
	    Cell cell = cells.get(cellId);
	    CellState cellState = cell.getCellState();
	    if(cellState.energy() >= molEnergy * chemStr.length()) {
		if(offX < -1 || offX > 1 || offY < -1 || offY > 1 || 
		   cellState.getX() + offX < 0 || cellState.getX() + offX >= width ||
		   cellState.getY() + offY < 0 || cellState.getY() + offY >= height) {
		    logger.info("DROPCHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") error illegal drop");
		    cell.sendMessage(new String[]{Cell.ERROR, "Illegal drop"});
		} else if(!chemStr.startsWith(config.getVal(Configuration.RECEPTORS, Configuration.START)) ||
			  !chemStr.endsWith(config.getVal(Configuration.RECEPTORS, Configuration.STOP)) ||
			  dnaPattern.matcher(chemStr).matches() ||
			  (cellState.type() == CellState.SUSCEPTIBLE && !chemStr.startsWith(anyStart)) ||
			  (cellState.type() == CellState.INFECTED && !chemStr.startsWith(infectedStart) && 
			   !chemStr.equals(anyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION) +
					   config.getVal(Configuration.RECEPTORS, Configuration.STOP))) ||
			  (cellState.type() == CellState.ANTIBODY && !chemStr.startsWith(antibodyStart) &&
			   !chemStr.equals(anyStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION) +
					   config.getVal(Configuration.RECEPTORS, Configuration.STOP)))) {
		    logger.info("DROPCHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") error malformed chemical");
		    cell.sendMessage(new String[]{Cell.ERROR, "Malformed chemical"});
		} else if(isStateChangeChemical(chemStr) &&
			  cellState.lastStateDrop() != -1 && (cellState.lastStateDrop() + stateDropTimeout > System.currentTimeMillis())) {
		    logger.info("DROPCHEM " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + 
				") error rapid drop: " + cellState.lastStateDrop());
		    cell.sendMessage(new String[]{Cell.ERROR, "Rapid infection or antibody drop"});
		} else {
		    Chemical newChem = new Chemical(chemStr, System.currentTimeMillis(), cellState.getX() + offX, cellState.getY() + offY, color);
		    newChem.setLifetime(getLifetime(chemStr));
		    if(isStateChangeChemical(chemStr)) {
			cellState.setLastStateDrop(System.currentTimeMillis());
			logger.debug("STATECHANGE drop: " + cellId + " (" + cellState.getX() + ", " + cellState.getY() + ")");
		    }
		    addChemical(newChem);
		    if(cellState.type() != CellState.SUSCEPTIBLE) {
			int energyConsumed = getProdEnergy(chemStr);
			cellState.setEnergy(cellState.energy() - energyConsumed);
			if(cellState.type() == CellState.INFECTED) {
			    infectedEnergyUse += ((double)energyConsumed / numInfected);
			} else if(cellState.type() == CellState.ANTIBODY) {
			    antibodyEnergyUse += ((double)energyConsumed / numAntibodies);
			}
			updateWindowedMetrics();
		    }
		    logger.info("DROPCHEM: " + cellId + " (" + cellState.getX() + ", " + cellState.getY() + ") " + chemStr + " - Ok");
		    sendAck(Cell.DROPCHEM, cellState, cell);
		}
	    } else {
		logger.info("DROPCHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + 
			    ") error not enough energy to produce chemical");
		cell.sendMessage(new String[]{Cell.ERROR, "Not enough energy to produce chemical"});
		cell.sendMessage(new String[]{Cell.EXIT + "=" + cellId});
	    }
	} else {
	    if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN)) {
		Cell cell = cells.get(cellId);
		if(cell != null) {
		    logger.info("DROPCHEM: " + cellId + "(" + cell.getCellState().getX() + ", " + 
				cell.getCellState().getY() + ") error bad arguments in drop chemical");
		    cell.sendMessage(new String[]{Cell.ERROR, "Bad arguments in drop chemical"});
		}
	    }
	}
    }

    public void senseChem(String cellId, String chemMatch, int maxChem, int offX, int offY) throws Exception {
	if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN) && maxChem > 0 && cells.get(cellId) != null &&
	   (offX == Cell.UNDEF || offX >= -1 && offX <= 1) && (offY == Cell.UNDEF || offY >= -1 && offY <= 1)) {
	    Cell cell = cells.get(cellId);
	    CellState cellState = cell.getCellState();
	    if(!chemMatch.startsWith(anyStart) && (cellState.type() == CellState.INFECTED && 
						   !chemMatch.startsWith(infectedStart) && !chemMatch.startsWith(antibodyStart)||
						   cellState.type() == CellState.ANTIBODY && 
						   !chemMatch.startsWith(antibodyStart) && !chemMatch.startsWith(infectedStart))) {
		logger.info("SENSECHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") illegal receptor to sense");
		cell.sendMessage(new String[]{Cell.ERROR, "Illegal receptor request"});
	    } else if(cellState.energy() >= senseEnergy * maxChem) {
		Chemical[] newChems = new Chemical[]{};
		if(offX == Cell.UNDEF || offY == Cell.UNDEF) {
		    newChems = getChemicals(cellState.getX(), cellState.getY(), chemMatch, maxChem);
		} else if(cellState.getX() + offX < 0 || cellState.getX() + offX >= width ||
			  cellState.getY() + offY < 0 || cellState.getY() + offY >= height) {
		    logger.info("SENSECHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") sensing out of bounds");
		    cell.sendMessage(new String[]{Cell.ERROR, "Request out of bounds"});
		    return;
		} else {		    
		    newChems = getChemicals(cellState.getX(), cellState.getY(), chemMatch, maxChem, offX, offY);
		}
		int energyConsumed = getSenseEnergy(newChems);
		if(cellState.type() == CellState.INFECTED) {
		    infectedEnergyUse += ((double)energyConsumed / numInfected);
		} else if(cellState.type() == CellState.ANTIBODY) {
		    antibodyEnergyUse += ((double)energyConsumed / numAntibodies);
		}
		updateWindowedMetrics();
		cellState.setEnergy(cellState.energy() - energyConsumed);
		String[] strarr = new String[newChems.length * 3 + 3];
		// XXX should send an OK string first, according to the documentation
		strarr[0] = Cell.CHEMICALS + "=" + newChems.length;
		for(int i = 0; i < newChems.length; i++) {
		    strarr[i * 3 + 1] = newChems[i].chemStr();
		    strarr[i * 3 + 2] = "offx=" + (newChems[i].getX() - cellState.getX());
		    strarr[i * 3 + 3] = "offy=" + (newChems[i].getY() - cellState.getY());
		}
		strarr[newChems.length * 3 + 1] = "type=" + cellState.type();
		strarr[newChems.length * 3 + 2] = "energy=" + cellState.energy();
		logger.info("SENSECHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + ") " + 
			    chemMatch + " - " + newChems.length + " - Ok");
		cell.sendMessage(strarr);
	    } else {
		logger.info("SENSECHEM: " + cellId + "(" + cellState.getX() + ", " + cellState.getY() + 
			    ") error not enough energy to sense chemical");
		cell.sendMessage(new String[]{Cell.ERROR, "Not enough energy to sense chemicals"});
		cell.sendMessage(new String[]{Cell.EXIT + "=" + cellId});
	    }
	} else {
	    if(!cellId.equals("") && !cellId.equals(Cell.UNKNOWN)) {
		Cell cell = cells.get(cellId);
		if(cell != null) {
		    logger.info("SENSECHEM: " + cellId + "(" + cell.getCellState().getX() + ", " + 
				cell.getCellState().getY() + "): " + offX + ", " + offY + ". error bad arguments in sense chemical");
		    cell.sendMessage(new String[]{Cell.ERROR, "Bad arguments in sense chemical"});
		}
	    }
	}
    }

    public void dropEnergy(String cellId, Cell cell, String chemStr, int x, int y) throws Exception {
	if(chemStr.startsWith(energyAffector) && cell != null && x >= 0 && y >= 0 && x < width && y < height) {
	    Chemical newChem = new Chemical(chemStr, System.currentTimeMillis(), x, y, 
					    config.getVal(Configuration.COLORS, energyAffector));
	    newChem.setLifetime(getLifetime(chemStr));

	    if(chemicalMap[y][x] == null)
		chemicalMap[y][x] = new ArrayList<Chemical>();
	    (chemicalMap[y][x]).add(newChem);
	    display.addChem(x, y, newChem.color());

	    logger.info("DROPENERGY: " + cellId + " (" + x + ", " + y + ") " + chemStr + " - Ok");
	    cell.sendMessage(new String[]{Cell.OK, Cell.DROPENERGY});
	} else {
	    logger.info("DROPENERGY: " + cellId + "(" + x + ", " + y + ") error bad arguments in drop energy");
	    cell.sendMessage(new String[]{Cell.ERROR, "Bad arguments in drop energy"});
	}
    }

    public void terminationTimeout(long termination) {
	this.termination = termination;
    }
    
    protected void removeCell(String cellId) {
	Cell cell = cells.get(cellId);
	if(cell != null) {
	    CellState cellState = cell.getCellState();
	    if(cellState.type() == CellState.SUSCEPTIBLE)
		numSusceptible--;
	    else if(cellState.type() == CellState.INFECTED)
		numInfected--;
	    else if(cellState.type() == CellState.ANTIBODY)
		numAntibodies--;

	    numCells--;
	    cellMapRemove(cellId, cellState.getX(), cellState.getY());
	    cells.remove(cellId);
	    cell.safeCloseConnection();

	    display.updateFrequentMetrics(numSusceptible, numInfected, numAntibodies, numInfections, numImmunisations);

	    if(numSusceptible == 0 && termination == 1) {
		try {
		    exitSimul();
		    System.exit(0);
		} catch(Exception e) {
		    System.exit(-1);
		}
	    }
	}
    }

    protected void addCell(String cellId, Cell cell) {
	CellState cellState = cell.getCellState();
	if(cellState.type() == CellState.SUSCEPTIBLE)
	    numSusceptible++;
	else if(cellState.type() == CellState.INFECTED)
	    numInfected++;
	else if(cellState.type() == CellState.ANTIBODY)
	    numAntibodies++;

	numCells++;
	cells.put(cellId, cell);
	cellMapAdd(cellId, cellState.getX(), cellState.getY());
	
	display.updateFrequentMetrics(numSusceptible, numInfected, numAntibodies, numInfections, numImmunisations);
    }

    protected void addChemical(Chemical chem) throws Exception {
	int x = chem.getX();
	int y = chem.getY();
	if(chemicalMap[y][x] == null)
	    chemicalMap[y][x] = new ArrayList<Chemical>();
	(chemicalMap[y][x]).add(chem);

	display.addChem(x, y, chem.color());
	
	for(int i = Math.max(x - 2 * senseRad, 0); i <= Math.min(x + 2 * senseRad, width - 1); i++)
	    for(int j = Math.max(y - 2 * senseRad, 0); j <= Math.min(y + 2 * senseRad, height - 1); j++)
		if(chemicalMap[j][i] != null && (chemicalMap[j][i]).size() > 0)
		    for(Iterator<Chemical> it = (chemicalMap[j][i]).iterator(); it.hasNext();) {
			Chemical chemical = it.next();
			if(chemical.creationTime() + chemical.getLifetime() < System.currentTimeMillis()) {
			    logger.debug("addChemical: Removing expired chemical at " + i + ", " + j);
			    display.removeChem(chemical.getX(), chemical.getY(), chemical.color());
			    it.remove();
			}
		    }

	int[] cellTypeSrc = new int[]{}; // XXX remove new declaration
	int cellTypeTgt = -1;
	if(chem.chemStr().startsWith(anyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION))) {
	    cellTypeSrc = new int[]{CellState.SUSCEPTIBLE, CellState.ANTIBODY};
	    cellTypeTgt = CellState.INFECTED;
	} else if(chem.chemStr().startsWith(antibodyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION))) {
	    cellTypeSrc = new int[]{CellState.ANTIBODY};
	    cellTypeTgt = CellState.INFECTED;
	} else if(chem.chemStr().startsWith(anyStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION))) {
	    cellTypeSrc = new int[]{CellState.SUSCEPTIBLE, CellState.INFECTED};
	    cellTypeTgt = CellState.ANTIBODY;
	} else if(chem.chemStr().startsWith(infectedStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION))) {
	    cellTypeSrc = new int[]{CellState.INFECTED};
	    cellTypeTgt = CellState.ANTIBODY;
	}
	
	boolean stateChanged = false;
	if(cellTypeSrc.length > 0)
	    for(int i = Math.max(x - senseRad, 0); i <= Math.min(x + senseRad, width - 1) && !stateChanged; i++)
		for(int j = Math.max(y - senseRad, 0); j <= Math.min(y + senseRad, height - 1) && !stateChanged; j++)
		    if(cellMap[j][i] != null && (cellMap[j][i]).size() > 0)
			for(Iterator<String> it = (cellMap[j][i]).iterator(); it.hasNext();) {
			    String cellId = it.next();
			    Cell cell = cells.get(cellId);
			    CellState cellState = cell.getCellState();
			    int k = 0;
			    for(int cellType: cellTypeSrc)
				if(cellState.type() == cellType)
				    if(stateChanged = changeState(cellState, cellTypeTgt))
					break;
			}
    }

    protected Chemical[] getChemicals(int x, int y, String chemMatch, int maxChem, int offX, int offY) {
	ArrayList<Chemical> arr = new ArrayList<Chemical>(); // XXX remove new declaration
	if(chemicalMap[y + offY][x + offX] != null && (chemicalMap[y + offY][x + offX]).size() > 0) {
	    for(Iterator<Chemical> it = (chemicalMap[y + offY][x + offX]).iterator(); it.hasNext();) {
		Chemical chem = it.next();
		if(chem.creationTime() + chem.getLifetime() >= System.currentTimeMillis()) {
		    if(chem.chemStr().startsWith(chemMatch))
			arr.add(chem);
		} else {
		    logger.debug("getChemicals: Removing expired chemical at " + x + ", " + y);
		    display.removeChem(chem.getX(), chem.getY(), chem.color());
		    it.remove();
		}
	    }
	}

	return getChemicalsCleanup(arr, maxChem);
    }

    protected Chemical[] getChemicals(int x, int y, String chemMatch, int maxChem) {
	ArrayList<Chemical> arr = new ArrayList<Chemical>(); // XXX remove new declaration
	for(int i = Math.max(x - senseRad, 0); i <= Math.min(x + senseRad, width - 1); i++)
	    for(int j = Math.max(y - senseRad, 0); j <= Math.min(y + senseRad, height - 1); j++)
		if(chemicalMap[j][i] != null && (chemicalMap[j][i]).size() > 0) {
		    for(Iterator<Chemical> it = (chemicalMap[j][i]).iterator(); it.hasNext();) {
			Chemical chem = it.next();
			if(chem.creationTime() + chem.getLifetime() >= System.currentTimeMillis()) {
			    if(chem.chemStr().startsWith(chemMatch))
				arr.add(chem);
			} else {
			    logger.debug("getChemicals: Removing expired chemical at " + i + ", " + j);
			    display.removeChem(chem.getX(), chem.getY(), chem.color());
			    it.remove();
			}
		    }
		}

	return getChemicalsCleanup(arr, maxChem);
    }

    private Chemical[] getChemicalsCleanup(ArrayList<Chemical> arr, int maxChem) {
	Chemical[] ret = new Chemical[]{}; // XXX remove new declaration
	if(arr.size() <= maxChem) {
	    ret = arr.toArray(new Chemical[]{});
	} else {
	    Chemical[] retarr = new Chemical[maxChem];
	    for(int i = 0; i < maxChem; i++) {
		int tap = (int)(Math.random() * arr.size());
		retarr[i] = arr.get(tap);
		arr.remove(tap);
	    }		
	    ret = retarr;
	}

	for(Chemical chem: ret) {
	    logger.debug("getChemicals: Removing picked-up chemical at " + chem.getX() + ", " + chem.getY());
	    (chemicalMap[chem.getY()][chem.getX()]).remove(chem);
	    display.removeChem(chem.getX(), chem.getY(), chem.color());
	}
 
	return ret;
    }

    protected boolean changeState(CellState cellState, int cellTypeTgt) throws Exception {
	String[] chemMatches = new String[]{}; // XXX remove new declaration
	if(cellState.type() == CellState.SUSCEPTIBLE && cellTypeTgt == CellState.INFECTED)
	    chemMatches = new String[]{anyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION) +
	                               config.getVal(Configuration.RECEPTORS, Configuration.STOP)};
	else if(cellState.type() == CellState.ANTIBODY && cellTypeTgt == CellState.INFECTED)
	    chemMatches = new String[]{anyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION) +
				       config.getVal(Configuration.RECEPTORS, Configuration.STOP),
				       antibodyStart + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION) +
				       config.getVal(Configuration.RECEPTORS, Configuration.STOP)};
	else if(cellState.type() == CellState.SUSCEPTIBLE && cellTypeTgt == CellState.ANTIBODY)
	    chemMatches = new String[]{anyStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION) +
				       config.getVal(Configuration.RECEPTORS, Configuration.STOP)};
	else if(cellState.type() == CellState.INFECTED && cellTypeTgt == CellState.ANTIBODY)
	    chemMatches = new String[]{anyStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION) +
				       config.getVal(Configuration.RECEPTORS, Configuration.STOP),
				       infectedStart + config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION) +
				       config.getVal(Configuration.RECEPTORS, Configuration.STOP)};

	ArrayList<Chemical> arr = new ArrayList<Chemical>(); // XXX remove new declaration
	for(int i = Math.max(cellState.getX() - senseRad, 0); i <= Math.min(cellState.getX() + senseRad, width - 1); i++)
	    for(int j = Math.max(cellState.getY() - senseRad, 0); j <= Math.min(cellState.getY() + senseRad, height - 1); j++)
		if(chemicalMap[j][i] != null && (chemicalMap[j][i]).size() > 0)
		    for(Iterator<Chemical> it = (chemicalMap[j][i]).iterator(); it.hasNext();) {
			Chemical chem = it.next();
			for(String chemMatch: chemMatches)
			    if(chem.chemStr().startsWith(chemMatch)) {
				arr.add(chem);
				break;
			    }
		    }

	logger.debug("changeState called for " + cellState.cellId() + " (" + cellState.getX() + ", " + cellState.getY() + ") " + 
		     cellState.type() + "->" + cellTypeTgt + " found " + arr.size());
	
	if(arr.size() >= quorumSize) {
	    for(int i = 0; i < quorumSize; i++) {
		(chemicalMap[arr.get(i).getY()][arr.get(i).getX()]).remove(arr.get(i));
		display.removeChem(arr.get(i).getX(), arr.get(i).getY(), arr.get(i).color());
	    }

	    cells.get(cellState.cellId()).sendMessage(new String[]{Cell.CHANGESTATE + "=" + cellState.cellId(),
								   "x=" + cellState.getX(), "y=" + cellState.getY(), 
								   "type=" + cellTypeTgt, "energy=" + cellState.energy()});
	    // XXX this will get removed when the cell rejoins in its new state and removes previous state
	    //	    display.remove(cellState.getX(), cellState.getY(), cellState.type());
	    logger.infooverride("STATECHANGE: crash expected " + cellState.cellId() + 
				"(" + cellState.getX() + ", " + cellState.getY() + ") - " + cellTypeTgt);

	    if((cellState.type() == CellState.SUSCEPTIBLE || cellState.type() == CellState.INFECTED) &&
	       cellTypeTgt == CellState.ANTIBODY)
		numImmunisations++;
	    else if((cellState.type() == CellState.SUSCEPTIBLE || cellState.type() == CellState.ANTIBODY) &&
		    cellTypeTgt == CellState.INFECTED)
		numInfections++;
	    
	    display.updateFrequentMetrics(numSusceptible, numInfected, numAntibodies, numInfections, numImmunisations);

	    return true;
	}
	
	return false;
    }

    protected void updateCoordinationMoves(CellState cellState1) {
	int x = cellState1.getX();
	int y = cellState1.getY();
	int type = cellState1.type();
	Hashtable<String, Double> moves = null;
	int numCells = 0;
	if(type == CellState.INFECTED) {
	    moves = infectedMoves;
	    numCells = numInfected;
	} else if(type == CellState.ANTIBODY) {
	    moves = antibodyMoves;
	    numCells = numAntibodies;
	} else
	    return;

	for(int i = Math.max(x - senseRad, 0); i <= Math.min(x + senseRad, width - 1); i++)
	    for(int j = Math.max(y - senseRad, 0); j <= Math.min(y + senseRad, height - 1); j++)
		if(cellMap[j][i] != null && (cellMap[j][i]).size() > 0)
		    for(Iterator<String> it = (cellMap[j][i]).iterator(); it.hasNext();) {
			String cellId = it.next();
			Cell cell = cells.get(cellId);
			CellState cellState2 = cell.getCellState();
			if(cellState2.type() == type) {
			    String key = cellState1.cellId() + ":" + cellState2.cellId();
			    double d = 0;
			    if(moves.get(key) != null)
				d = moves.get(key);
			    d += (1.0 / numCells);
			    moves.put(key, d);
			}
		    }
    }

    public void updateCoordinationMetrics() {
	display.updateCoordinationMetrics(computeCoordination(infectedMoves), 
					  computeCoordination(antibodyMoves));
    }

    public synchronized void exitSimul() throws Exception {
	logger.infooverride("SERVEREXITING: " + System.currentTimeMillis());
	logger.infooverride("EXITSTATUS#" + numSusceptible + "#" + numInfected + "#" + numAntibodies);
	Enumeration<String> e = cells.keys();
	while(e.hasMoreElements()) {
	    String cellId = e.nextElement();
	    Cell cell = cells.get(cellId);
	    logger.infooverride("Sending EXIT message to cell " + cellId);
	    cell.sendMessage(new String[]{Cell.EXIT + "=" + cellId});
	}
    }

    private double computeCoordination(Hashtable<String, Double> moves) {
	double coordination = 0;
	Enumeration<String> e = moves.keys();
	while(e.hasMoreElements()) {
	    coordination += moves.get(e.nextElement());
	}
	return coordination / (System.currentTimeMillis() - simulationStart) * 60000;
    }

    private void updateWindowedMetrics() {
	long currentTime = System.currentTimeMillis();
	if(currentTime - lastWindowUpdate > updateWindow) {
	    lastWindowUpdate = currentTime;
	    lastInfectedEnergyUse = energyAlpha * lastInfectedEnergyUse + (1 - energyAlpha) * infectedEnergyUse;
	    lastAntibodyEnergyUse = energyAlpha * lastAntibodyEnergyUse + (1 - energyAlpha) * antibodyEnergyUse;
	    infectedEnergyUse = 0;
	    antibodyEnergyUse = 0;
	    display.updateWindowedMetrics(lastInfectedEnergyUse, lastAntibodyEnergyUse);
	}
    }

    private boolean isStateChangeChemical(String chemStr) {
	String stateChangeChunk = chemStr.substring(anyStart.length(), 
						    anyStart.length() + config.getVal(Configuration.AFFECTORS, Configuration.INFECTION).length());
	if(stateChangeChunk.equals(config.getVal(Configuration.AFFECTORS, Configuration.INFECTION)) ||
	   stateChangeChunk.equals(config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION)))
	    return true;
	else
	    return false;
    }

    private long getLifetime(String chemStr) {
	if(chemStr.indexOf(config.getVal(Configuration.AFFECTORS, Configuration.PHEROMONE), 
			   anyStart.length()) == anyStart.length())
	    return pheromoneLifetime;
	else
	    return chemStr.length() * molTimeout;
    }

    private int getProdEnergy(String chemStr) {
	if(chemStr.indexOf(config.getVal(Configuration.AFFECTORS, Configuration.INFECTION), anyStart.length()) == anyStart.length())
	    return infectionEnergy;
	else if(chemStr.indexOf(config.getVal(Configuration.AFFECTORS, Configuration.IMMUNISATION), anyStart.length()) == anyStart.length())
	    return immunisationEnergy;
	else
	    return chemStr.length() * molEnergy;
    }

    private int getSenseEnergy(Chemical[] chems) {
	int deficit = chems.length * senseEnergy;
	for(int i = 0; i < chems.length; i++)
	    if(chems[i].chemStr().startsWith(energyReceptor))
		deficit -= transferEnergy;
	return deficit;
    }

    private void sendAck(String msgType, CellState cellState, Cell cell) throws Exception {
	cell.sendMessage(new String[]{Cell.OK, msgType, "type=" + cellState.type(), 
				      "energy=" + cellState.energy()});
    }

    private void cellMapAdd(String cellId, int x, int y) {
	if(cellMap[y][x] == null)
	    cellMap[y][x] = new ArrayList<String>();
	(cellMap[y][x]).add(cellId);
	if(cells.get(cellId) != null)
	    display.add(x, y, cells.get(cellId).getCellState().type(), cellId);
    }

    private void cellMapRemove(String cellId, int x, int y) {
	if(cellMap[y][x] != null) {
	    (cellMap[y][x]).remove(cellId);
	    if(cells.get(cellId) != null)
		display.remove(x, y, cells.get(cellId).getCellState().type());
	}
    }

    private void printChemMap() {
	for(int j = 0; j < height; j++) {
	    for(int i = 0; i < width; i++) {
		if(chemicalMap[j][i] != null && chemicalMap[j][i].size() > 0)
		    System.out.print("*");
		else
		    System.out.print("-");
	    }
	    System.out.println();
	}
    }

    private void printCellMap() {
	for(int j = 0; j < height; j++) {
	    for(int i = 0; i < width; i++) {
		if(cellMap[j][i] != null && (cellMap[j][i]).size() > 0) {
		    if((cellMap[j][i]).size() > 1)
			System.out.print((cellMap[j][i]).size());
		    else if(cells.get((cellMap[j][i]).get(0)).getCellState().type() == CellState.SUSCEPTIBLE)
			System.out.print("s");
		    else if(cells.get((cellMap[j][i]).get(0)).getCellState().type() == CellState.INFECTED)
			System.out.print("i");
		    else if(cells.get((cellMap[j][i]).get(0)).getCellState().type() == CellState.ANTIBODY)
			System.out.print("a");
		} else
		    System.out.print(".");
	    }
	    System.out.println();
	}
    }

}